package dse.modules;

import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Random;
import java.util.Vector;

import testing.TestHelper;
import dse.Network;
import dse.Cluster;
import dse.DataObject;
import dse.DseException;
import dse.ExperimentConfiguration;
import dse.Node;
import dse.NodeState;
import dse.modules.eln.EarthLikeNetwork;

public class SimpleAssigner implements Assigner {
	
	private int [] hashRoots; 

	public void initalize (
			ExperimentConfiguration systemSettings, 
			Map<String, String> moduleSettings
			)
	throws DseException
	{
		ModuleUtil.SettingsReader sr = ModuleUtil.getSettingsReader(
				this.getClass(), moduleSettings
				);
		int algs = sr.getOptionalInteger("hash-algs-for-data-objects", 1);
		assert algs > 0;
		
		hashRoots = new int [algs];
		Random r = systemSettings.getRandom();
		for (int a = 0; a < algs; a++)
			hashRoots[a] = r.nextInt();		
	}
		
	/**
	 * This should use a hashing technique to assign the object to a cluster in
	 * the network.
	 * 
	 * @param dataObject	The object to assign.
	 * @param clusters		The clusters.
	 * @param maxClusterId	
	 * 
	 * @return	The node that should host the data object.  Note that this could
	 *  		potentially be null in some cases.	
	 */
	public Iterable<Cluster> assign (
			DataObject dataObject,
			Iterable<Cluster> clusters,
			int maxClusterId
			)
	{
		Vector<Cluster> mappings = new Vector<Cluster>(hashRoots.length);
		for (int a = 0; a < hashRoots.length; a++) {
			//Integer hr = hashRoots[a];
			//Random random = new Random(123456);
			int maxId = maxClusterId;
			int x = (dataObject.id()) + a*maxId/dataObject.shareCount();
			
			int hashToId = (x%maxId) + 1;
			
			for(Cluster c : clusters)
			{
				if(c.id() == hashToId)
				{
					mappings.add(c);
					break;
				}
			}
				
			/*Cluster lastPreceeder = null;
			boolean mapped = false;
			for (Cluster c : clusters) {
				if (lastPreceeder == null)
					lastPreceeder = c;
				if (c.id() <= hashToId)
					lastPreceeder = c;
				else if (lastPreceeder != null) 
				{
					mapped = true;
					if (mappings.contains(lastPreceeder) == false)
						mappings.add(lastPreceeder);
					break;
				}	
			}	
			if (!mapped && mappings.contains(lastPreceeder) == false)
				mappings.add(lastPreceeder);*/
		}
		return mappings;
	}
	
	/**
	 * This should use a hashing technique to assign the object to a node in
	 * the network.
	 * 
	 * @param dataObject	The object to assign.
	 * @param network		The network.	
	 * 
	 * @return	The node that should host the data object.  Note that this could
	 *  		potentially be null in some cases.	
	 */
	public Iterable<Node> assign (
			DataObject dataObject,
			Network network
			)
	{
		Vector<Node> mappings = new Vector<Node>(hashRoots.length);
		for (int a = 0; a < hashRoots.length; a++) {
			//int hr = hashRoots[a];
			int maxId = network.maxNodeId();
			//int x = ((int)(dataObject.hashCode()) ^ hr);
			int x = dataObject.id() + a;
			int hashToId = (x%maxId) + 1;
			
			mappings.add(network.lookupNode(hashToId));
		}
		return mappings;
			/*Node lastPreceeder = null;
			boolean mapped = false;
			Iterable<Node> nodes = network.nodes();
			for (Node n : nodes) {
				if (lastPreceeder == null)
					lastPreceeder = n;
				if (n.id() <= hashToId)
					lastPreceeder = n;
				else if (lastPreceeder != null) 
				{
					mapped = true;
					if (mappings.contains(lastPreceeder) == false)
						mappings.add(lastPreceeder);
					break;
				}	
			}	
			if (!mapped && mappings.contains(lastPreceeder) == false)
				mappings.add(lastPreceeder);
		}
		return mappings;*/
	}
	
	/**
	 * This should use a hashing technique to assign the object to a node of the
	 * cluster.
	 * 
	 * @param dataObject	The object to assign.
	 * @param cluster
	 * 
	 * @return	
	 */
	public Node assign (
			DataObject dataObject,
			Cluster cluster 
			)
	{
		return assign (dataObject, 1, cluster);
	}
	/**
	 * This should use a hashing technique to assign the object to a node of the
	 * cluster.
	 * 
	 * @param dataObject	The object to assign.
	 * @param shareIndex	The share to assign (starts at 1).
	 * @param cluster
	 * 
	 * @return	The node that should host the share.  Note that this could
	 *  		potentially be null in some cases.	
	 */
	public Node assign (
			DataObject dataObject,
			int shareIndex,
			Cluster cluster 
			)
	{
		//Random random = new Random(123456);
		int m = cluster.maxLocalNodeId();
		
		/*int hashToId = (int)(
				Math.abs(
						Math.pow(
								dataObject.id()+shareIndex,
								2.46+Math.sqrt(shareIndex)
								)
						+1)
				% m
				);*/
		int x = dataObject.id() + shareIndex*m/dataObject.shareCount();		
		int hashToId = (x%m) + 1;
			
		/*Node lastPreceeder = null;
		for (Node n : cluster.nodes()) {
			// This can happen if it wasnt initalized properly
			assert n.isUp() || n.isDown(); 
			
			//if (n.isUp() == false)
				//continue;
			
			if (lastPreceeder == null)
				lastPreceeder = n;
			if (n.localId() <= hashToId)
				lastPreceeder = n;
			else if (lastPreceeder != null)
				return lastPreceeder;
				
		}
		assert lastPreceeder != null;
		return lastPreceeder;*/
		return cluster.lookupNode(hashToId);
	}
	/**
	 * This should hash the data object shares to appropriate nodes and host
	 * them on those nodes.  If enough valid nodes are found then the cluster's
	 * hosted data object list should be updated and this should return true.
	 * If there are not enough valid share hosts, then this should return false.
	 * 
	 * @param cluster
	 * @param dataObject
	 * @return				Whether or not the operation succeeded.
	 */
	public boolean hostShares (Cluster cluster, DataObject dataObject) {
		Map<Node, Integer> hosts = new Hashtable<Node, Integer>();
		LinkedList<Node> invalidHosts = new LinkedList<Node>();
		
		for (int si = 1; si <= dataObject.shareCount(); si++) {
			Node h = this.assign(dataObject, si, cluster);
			
			if (hosts.containsKey(h))
				invalidHosts.add(h);
			else
				hosts.put(h, si);
		}
		
		for (Node h  : invalidHosts)
			hosts.remove(h);
		
		if (hosts.size() < dataObject.requiredShareCount())
			return false;
		
		for (Node h : hosts.keySet()) {
			int si = hosts.get(h);
			h.host(dataObject, si);
		}
		
		//cluster.host(dataObject);
		return true;
	}
	
	public Hashtable<Node, Integer> hostedShares (Cluster cluster, DataObject dataObject) {
		Hashtable<Node, Integer> hosts = new Hashtable<Node, Integer>();
		LinkedList<Node> invalidHosts = new LinkedList<Node>();
		
		for (int si = 1; si <= dataObject.shareCount(); si++) {
			Node h = this.assign(dataObject, si, cluster);
			
			if (hosts.containsKey(h))
				invalidHosts.add(h);
			else
				hosts.put(h, si);
		}
		
		for (Node h  : invalidHosts)
			hosts.remove(h);
		
		if (hosts.size() < dataObject.requiredShareCount())
			return null;
		
		return hosts;
	}
	
	/**
	 * This should use a hashing technique to assign the object to a node of the
	 * network without clusters.
	 * 
	 * @param dataObject	The object to assign.
	 * @param shareIndex	The share to assign (starts at 1).
	 * @param network		The network
	 * @return	The node that should host the share.  Note that this could
	 *  		potentially be null in some cases.	
	 */
	public Node assign (
			DataObject dataObject,
			int shareIndex,
			Network network
			)
	{
		//Random random = new Random(123456);
		int m = network.maxNodeId();
		//int x = (int)(Math.abs(Math.pow(dataObject.id()+shareIndex,2.46+Math.sqrt(shareIndex))));
		int x = dataObject.id() + shareIndex*m/dataObject.shareCount();
		//int x = dataObject.id() + shareIndex;
		int hashToId = x%m + 1;
			
		/*Node lastPreceeder = null;
		for (Node n : network.nodes()) {
			// This can happen if it wasnt initalized properly
			assert n.isUp() || n.isDown(); 
			
			//if (n.isUp() == false)
				//continue;
			
			if (lastPreceeder == null)
				lastPreceeder = n;
			if (n.id() <= hashToId)
				lastPreceeder = n;
			else if (lastPreceeder != null)
				return lastPreceeder;
				
		}
		assert lastPreceeder != null;
		return lastPreceeder;*/
		return network.lookupNode(hashToId);
	}
	/**
	 * This should hash the data object shares to appropriate nodes and host
	 * them on those nodes.  If enough valid nodes are found then the cluster's
	 * hosted data object list should be updated and this should return true.
	 * If there are not enough valid share hosts, then this should return false.
	 * 
	 * @param network
	 * @param dataObject
	 * @return				Whether or not the operation succeeded.
	 */
	public boolean hostShares (Network network, DataObject dataObject) {
		Map<Node, Integer> hosts = new Hashtable<Node, Integer>();
		LinkedList<Node> invalidHosts = new LinkedList<Node>();
		//dataObject.setShareCount(dataObject.shareCount());
		for (int si = 1; si <= dataObject.shareCount(); si++) {
			Node h = this.assign(dataObject, si, network);
			
			if (hosts.containsKey(h))
			{
				invalidHosts.add(h);
				System.err.println("Duplicate share for data" + dataObject.id() + " share" + si + " Node" + h.id());
			}
			else
				hosts.put(h, si);
		}
		
		for (Node h  : invalidHosts)
		{
			hosts.remove(h);
		}
		
		if (hosts.size() < dataObject.requiredShareCount())
			return false;
		
		for (Node h : hosts.keySet()) {
			int si = hosts.get(h);
			h.host(dataObject, si);
		}
		return true;
	}
	
	public Hashtable<Node, Integer> hostedShares(Network network, DataObject dataObject)
	{
		Hashtable<Node, Integer> hosts = new Hashtable<Node, Integer>();
		LinkedList<Node> invalidHosts = new LinkedList<Node>();
		for (int si = 1; si <= dataObject.shareCount(); si++) {
			Node h = this.assign(dataObject, si, network);
			
			if (hosts.containsKey(h))
				invalidHosts.add(h);
			else
				hosts.put(h, si);
		}
		
		for (Node h  : invalidHosts)
			hosts.remove(h);
		
		return hosts;
	}
	
	/**
	 * For testing.
	 * 
	 * @param args
	 * @throws DseException
	 */
	public static void main (String [] args)
	throws DseException
	{
		// We need to test this bad boy, to make sure we arent rejecting valid
		// data objects from certain clusters :)
		if (args.length != 3 && args.length != 4) {
			System.out.println(
					"Usage: java -cp ... " +
					"dse.modules.SimpleAssigner " +
					"<nodes> <max-cluster-size> <num-data-objs> (seed)"
					);
			System.exit(-1);
		}
		
		ExperimentConfiguration ss = TestHelper.createSystemSettings(args, 3);
		
		EarthLikeNetwork network = TestHelper.createELNetwork(
				ss, new String [] {args[0], "20", "0.6"}, 0, 1, 2
				);
		
		TestHelper.createClusters(ss, args, 1, network);
		
		Vector<DataObject> dataObjects = TestHelper.createDataObjects(
				ss, args, 2, -1, -1, -1, -1
				);
		

		SimpleAssigner sa = new SimpleAssigner ();
		
		Hashtable<String, String> props = new Hashtable<String, String>();
		
		sa.initalize(ss, props);
		
		// Make sure all nodes are set to their default UP 
		for (Node n : network.nodes())
			n.setState(NodeState.Up);
		
		// Seems like our distribution is not right...
		Map<Cluster,Integer> counts = new Hashtable<Cluster, Integer>();
		
		Iterator<DataObject> d = dataObjects.iterator();
		while (d.hasNext()) {
			DataObject dobj = d.next();
			Cluster host = sa.assign(
					dobj, network.clusters(), network.maxClusterId()
					).iterator().next();
			
			if (counts.get(host) == null)
				counts.put(host, 1);
			else
				counts.put(host, 1 + counts.get(host));
			
			System.out.println(
					"Storing " + dobj.id() + " with " + dobj.shareCount() + 
					" shares in cluster " + host.id() + " size = " + host.size()
					);
			
			boolean success = sa.hostShares(host, dobj);
			if (!success) {
				d.remove();
				System.out.println(
						"Data Object #" + dobj.id() + 
						" could not be hosted in designated cluster #" + 
						host.id() + "."
						);
			} else {
				/*
				System.out.println(
						"Data Object #" + dobj.id() + 
						" hosted in cluster #" + 
						host.id() + "."
						);
				*/
			}
		}
		
		
		System.out.println();
		for (Cluster c : network.clusters())
			if (counts.get(c) != null)
				System.out.println("Cluster #" + c.id() + ":\t" + counts.get(c));
			else
				System.out.println("Cluster #" + c.id() + ":\tNO OBJECTS!");
	}
}